import { isBlock } from "@html_editor/utils/blocks";
import { getAdjacentPreviousSiblings } from "@html_editor/utils/dom_traversal";
import { blendColors } from "@web/core/utils/colors";

function parentsGet(node, root = undefined) {
    const parents = [];
    while (node) {
        parents.unshift(node);
        if (node === root) {
            break;
        }
        node = node.parentNode;
    }
    return parents;
}

function commonParentGet(node1, node2, root = undefined) {
    if (!node1 || !node2) {
        return null;
    }
    const n1p = parentsGet(node1, root);
    const n2p = parentsGet(node2, root);
    while (n1p.length > 1 && n1p[1] === n2p[1]) {
        n1p.shift();
        n2p.shift();
    }
    // Check  in case at least one of them is not in the DOM.
    return n1p[0] === n2p[0] ? n1p[0] : null;
}

//--------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------

const RE_COL_MATCH = /(^| )col(-[\w\d]+)*( |$)/;
const RE_OFFSET_MATCH = /(^| )offset(-[\w\d]+)*( |$)/;
const RE_PADDING_MATCH = /[ ]*padding[^;]*;/g;
const RE_PADDING = /([\d.]+)/;
const RE_WHITESPACE = /[\s\u200b]*/;
const SELECTORS_IGNORE = /(^\*$|:hover|:before|:after|:active|:link|::|')|@page/; // :not(:has) should be legal
const CONVERT_INLINE_BLACKLIST_CLASSES = ["o_mail_redirect"];
// CSS properties relating to font, which Outlook seem to have trouble inheriting.
const FONT_PROPERTIES_TO_INHERIT = [
    "color",
    "font-size",
    "font-family",
    "font-weight",
    "font-style",
    "text-decoration",
    "text-transform",
    "text-align",
];
// Attributes all tables should have in a mailing.
export const TABLE_ATTRIBUTES = {
    cellspacing: 0,
    cellpadding: 0,
    border: 0,
    width: "100%",
    align: "center",
    role: "presentation",
};
// Cancel tables default styles.
export const TABLE_STYLES = {
    "border-collapse": "collapse",
    "text-align": "inherit",
    "font-size": "unset",
    "line-height": "inherit",
};

export const BASIC_THEME_TABLE_STYLES = {
    "background-color": "transparent",
    color: "inherit",
};

const GROUPED_STYLES = {
    border: [
        "border-top-width",
        "border-right-width",
        "border-bottom-width",
        "border-left-width",
        "border-top-style",
        "border-right-style",
        "border-bottom-style",
        "border-left-style",
        "border-top-color",
        "border-right-color",
        "border-bottom-color",
        "border-left-color",
    ],
    padding: ["padding-top", "padding-bottom", "padding-left", "padding-right"],
    margin: ["margin-top", "margin-bottom", "margin-left", "margin-right"],
    "border-radius": [
        "border-top-left-radius",
        "border-top-right-radius",
        "border-bottom-right-radius",
        "border-bottom-left-radius",
    ],
};

//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------

/**
 * Convert snippets and mailing bodies to tables.
 *
 * @param {HTMLElement} element
 */
export function addTables(element) {
    const isInBasicTheme = Boolean(element.querySelector(".o_layout.o_basic_theme"));
    for (const snippet of element.querySelectorAll(".o_mail_snippet_general, .o_layout")) {
        if (isInBasicTheme) {
            for (const [property, value] of Object.entries(BASIC_THEME_TABLE_STYLES)) {
                snippet.style.setProperty(property, value);
            }
        }
        // Convert all snippets and the mailing itself into table > tr > td
        const table = _createTable(snippet.attributes);

        const row = document.createElement("tr");
        let col = document.createElement("td");
        row.appendChild(col);
        if (snippet.classList.contains("o_basic_theme")) {
            const div = document.createElement("div");
            div.classList.add("o_apple_wrapper_padding");
            col.appendChild(div);
            col = div;
            const style = document.createElement("style");
            // We create a nested media query because it's only supported by a
            // handful of clients, including Apple Mail, and we actually only
            // want this for Apple Mail.
            const padding = "34px"; // This is what's needed to align the content with Apple Mail's header.
            style.textContent =
                `@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding:${snippet.style.padding};}}}` +
                `@media(min-width:737px){@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding-left:${padding};}}}}`;
            div.before(style);
        }
        table.appendChild(row);

        for (const child of [...snippet.childNodes]) {
            col.appendChild(child);
        }
        snippet.before(table);
        snippet.remove();

        // If snippet doesn't have a table as child, wrap its contents in one.
        const childTables = [...col.children].filter((child) => child.nodeName === "TABLE");
        if (!childTables.length) {
            const tableB = _createTable();
            const rowB = document.createElement("tr");
            const colB = document.createElement("td");

            rowB.appendChild(colB);
            tableB.appendChild(rowB);
            for (const child of [...col.childNodes]) {
                colB.appendChild(child);
            }
            col.appendChild(tableB);
        }
    }
}
/**
 * Convert CSS display for attachment link to real image.
 * Without this post process, the display depends on the CSS and the picture
 * does not appear when we use the html without css (to send by email for e.g.)
 *
 * @param {HTMLElement} element
 */
function attachmentThumbnailToLinkImg(element) {
    const links = [
        ...element.querySelectorAll(`a[href*="/web/content/"][data-mimetype]:empty`),
    ].filter((link) => RE_WHITESPACE.test(link.textContent));
    for (const link of links) {
        const image = document.createElement("img");
        image.setAttribute(
            "src",
            _getStylePropertyValue(link, "background-image").replace(/(^url\(['"])|(['"]\)$)/g, "")
        );
        // Note: will trigger layout thrashing.
        image.setAttribute("height", Math.max(1, _getHeight(link)));
        image.setAttribute("width", Math.max(1, _getWidth(link)));
        link.prepend(image);
    }
}
/**
 * Convert Bootstrap rows and columns to actual tables.
 *
 * Note: Because of the limited support of media queries in emails, this doesn't
 * support the mixing and matching of column options (e.g., "col-4 col-sm-6" and
 * "col col-4" aren't supported).
 *
 * @param {Element} element
 */
export function bootstrapToTable(element) {
    // First give all rows in columns a separate container parent.
    for (const rowInColumn of [...element.querySelectorAll(".row")].filter((row) =>
        RE_COL_MATCH.test(row.parentElement.className)
    )) {
        const parentColumn = rowInColumn.parentElement;
        const previous = rowInColumn.previousElementSibling;
        if (previous && previous.classList.contains("o_fake_table")) {
            // If a container was already created there, append to it.
            previous.append(rowInColumn);
        } else {
            _wrap(rowInColumn, "div", "o_fake_table");
        }
        // Bootstrap rows have negative left and right margins, which are not
        // supported by GMail and Outlook. Add up the padding of the column with
        // the negative margin of the row to get the correct padding.
        const rowStyle = getComputedStyle(rowInColumn);
        const columnStyle = getComputedStyle(parentColumn);
        for (const side of ["left", "right"]) {
            const negativeMargin = +rowStyle[`margin-${side}`].replace("px", "");
            const columnPadding = +columnStyle[`padding-${side}`].replace("px", "");
            if (negativeMargin < 0 && columnPadding >= Math.abs(negativeMargin)) {
                parentColumn.style[`padding-${side}`] = `${columnPadding + negativeMargin}px`;
                rowInColumn.style[`margin-${side}`] = 0;
            }
        }
    }

    // These containers from the mass mailing masonry snippet require full
    // height contents, which is only possible if the table itself has a set
    // height. We also need to restyle it because of the change in structure.
    for (const masonryTopInnerContainer of element.querySelectorAll(
        ".s_masonry_block > .container"
    )) {
        masonryTopInnerContainer.style.setProperty("height", "100%");
    }
    for (const masonryGrid of element.querySelectorAll(".o_masonry_grid_container")) {
        masonryGrid.style.setProperty("padding", 0);
        for (const fakeTable of [...masonryGrid.children].filter((c) =>
            c.classList.contains("o_fake_table")
        )) {
            fakeTable.style.setProperty("height", _getHeight(fakeTable) + "px");
        }
    }
    for (const masonryRow of element.querySelectorAll(
        ".o_masonry_grid_container > .o_fake_table > .row.h-100"
    )) {
        masonryRow.style.removeProperty("height");
        masonryRow.parentElement.style.setProperty("height", "100%");
    }

    const containers = element.querySelectorAll(".container, .container-fluid, .o_fake_table");
    // Capture the widths of the containers before manipulating it.
    for (const container of containers) {
        container.setAttribute("o-temp-width", _getWidth(container));
    }
    // Now convert all containers with rows to tables.
    for (const container of [...containers].filter((n) =>
        [...n.children].some((c) => c.classList.contains("row"))
    )) {
        // The width of the table was stored in a temporary attribute. Fetch it
        // for use in `_applyColspan` and remove the attribute at the end.
        const containerWidth = parseFloat(container.getAttribute("o-temp-width"));

        // TABLE
        const table = _createTable(container.attributes);
        for (const child of [...container.childNodes]) {
            table.append(child);
        }
        table.classList.remove("container", "container-fluid", "o_fake_table");
        if (!table.className) {
            table.removeAttribute("class");
        }
        container.before(table);
        container.remove();

        // ROWS
        // First give all siblings of rows a separate row/col parent combo.
        for (const row of [...table.children].filter(
            (child) => isBlock(child) && !child.classList.contains("row")
        )) {
            const newCol = _wrap(row, "div", "col-12");
            _wrap(newCol, "div", "row");
        }

        for (const bootstrapRow of [...table.children].filter((c) => c.classList.contains("row"))) {
            const tr = document.createElement("tr");
            for (const attr of bootstrapRow.attributes) {
                tr.setAttribute(attr.name, attr.value);
            }
            tr.classList.remove("row");
            if (!tr.className) {
                tr.removeAttribute("class");
            }
            for (const child of [...bootstrapRow.childNodes]) {
                tr.append(child);
            }
            bootstrapRow.before(tr);
            bootstrapRow.remove();

            // COLUMNS
            const bootstrapColumns = [...tr.children].filter(
                (column) => column.className && column.className.match(RE_COL_MATCH)
            );

            // 1. Replace generic "col" classes with specific "col-n", computed
            //    by sharing the available space between them.
            const flexColumns = bootstrapColumns.filter(
                (column) => !/\d/.test(column.className.match(RE_COL_MATCH)[0] || "0")
            );
            const colTotalSize = bootstrapColumns
                .map((child) => _getColumnSize(child) + _getColumnOffsetSize(child))
                .reduce((a, b) => a + b, 0);
            const colSize = Math.max(1, Math.round((12 - colTotalSize) / flexColumns.length));
            for (const flexColumn of flexColumns) {
                flexColumn.classList.remove(flexColumn.className.match(RE_COL_MATCH)[0].trim());
                flexColumn.classList.add(`col-${colSize}`);
            }

            // 2. Create and fill up the row(s) with grid(s).
            // Create new, empty columns for column offsets.
            let columnIndex = 0;
            for (const bootstrapColumn of [...bootstrapColumns]) {
                const offsetSize = _getColumnOffsetSize(bootstrapColumn);
                if (offsetSize) {
                    const newColumn = document.createElement("div");
                    newColumn.classList.add(`col-${offsetSize}`);
                    bootstrapColumn.classList.remove(
                        bootstrapColumn.className.match(RE_OFFSET_MATCH)[0].trim()
                    );
                    bootstrapColumn.before(newColumn);
                    bootstrapColumns.splice(columnIndex, 0, newColumn);
                    columnIndex++;
                }
                columnIndex++;
            }
            let grid = _createColumnGrid();
            let gridIndex = 0;
            let currentRow = tr.cloneNode();
            tr.after(currentRow);
            let currentCol;
            columnIndex = 0;
            for (const bootstrapColumn of bootstrapColumns) {
                const columnSize = _getColumnSize(bootstrapColumn);
                if (gridIndex + columnSize < 12) {
                    currentCol = grid[gridIndex];
                    _applyColspan(currentCol, columnSize, containerWidth);
                    gridIndex += columnSize;
                    if (columnIndex === bootstrapColumns.length - 1) {
                        // We handled all the columns but there is still space
                        // in the row. Insert the columns and fill the row.
                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);
                        currentRow.append(...grid.filter((td) => td.getAttribute("colspan")));
                    }
                } else if (gridIndex + columnSize === 12) {
                    // Finish the row.
                    currentCol = grid[gridIndex];
                    _applyColspan(currentCol, columnSize, containerWidth);
                    currentRow.append(...grid.filter((td) => td.getAttribute("colspan")));
                    if (columnIndex !== bootstrapColumns.length - 1) {
                        // The row was filled before we handled all of its
                        // columns. Create a new one and start again from there.
                        const previousRow = currentRow;
                        currentRow = currentRow.cloneNode();
                        previousRow.after(currentRow);
                        grid = _createColumnGrid();
                        gridIndex = 0;
                    }
                } else {
                    // Fill the row with what was in the grid before it
                    // overflowed.
                    _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);
                    currentRow.append(...grid.filter((td) => td.getAttribute("colspan")));
                    // Start a new row that starts with the current col.
                    const previousRow = currentRow;
                    currentRow = currentRow.cloneNode();
                    previousRow.after(currentRow);
                    grid = _createColumnGrid();
                    currentCol = grid[0];
                    _applyColspan(currentCol, columnSize, containerWidth);
                    gridIndex = columnSize;
                    if (columnIndex === bootstrapColumns.length - 1 && gridIndex < 12) {
                        // We handled all the columns but there is still space
                        // in the row. Insert the columns and fill the row.
                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);
                        currentRow.append(...grid.filter((td) => td.getAttribute("colspan")));
                    }
                }
                if (currentCol) {
                    for (const attr of bootstrapColumn.attributes) {
                        if (attr.name !== "colspan") {
                            currentCol.setAttribute(attr.name, attr.value);
                        }
                    }
                    const colMatch = bootstrapColumn.className.match(RE_COL_MATCH);
                    currentCol.classList.remove(colMatch[0].trim());
                    if (!currentCol.className) {
                        currentCol.removeAttribute("class");
                    }
                    for (const child of [...bootstrapColumn.childNodes]) {
                        currentCol.append(child);
                    }
                    // Adapt width to colspan.
                    _applyColspan(currentCol, +currentCol.getAttribute("colspan"), containerWidth);
                }
                columnIndex++;
            }
            tr.remove(); // row was cloned and inserted already
        }
    }
    for (const table of element.querySelectorAll("table")) {
        table.removeAttribute("o-temp-width");
    }
    // Merge tables in tds into one common table, each in its own row.
    const tds = [...element.querySelectorAll("td")]
        .filter(
            (td) =>
                td.children.length > 1 &&
                [...td.children].every((child) => child.nodeName === "TABLE")
        )
        .reverse();
    for (const td of tds) {
        const table = _createTable();
        const trs = [...td.children]
            .map((child) => _wrap(child, "td"))
            .map((wrappedChild) => _wrap(wrappedChild, "tr"));
        trs[0].before(table);
        table.append(...trs);
    }
}
/**
 * Convert Bootstrap cards to table structures.
 *
 * @param {Element} element
 */
export function cardToTable(element) {
    for (const card of element.querySelectorAll(".card")) {
        const table = _createTable(card.attributes);
        table.style.removeProperty("overflow");
        const cardImgTopSuperRows = [];
        for (const child of [...card.childNodes]) {
            const row = document.createElement("tr");
            const col = document.createElement("td");
            if (!["IMG", "A"].includes(child.nodeName) && isBlock(child)) {
                for (const attr of child.attributes) {
                    col.setAttribute(attr.name, attr.value);
                }
                for (const descendant of [...child.childNodes]) {
                    col.append(descendant);
                }
                child.remove();
            } else if (child.nodeType === Node.TEXT_NODE) {
                if (child.textContent.replace(RE_WHITESPACE, "").length) {
                    col.append(child);
                } else {
                    continue;
                }
            } else {
                col.append(child);
            }
            const subTable = _createTable();
            const superRow = document.createElement("tr");
            const superCol = document.createElement("td");
            row.append(col);
            subTable.append(row);
            superCol.append(subTable);
            superRow.append(superCol);
            table.append(superRow);
            if (child.nodeType === Node.ELEMENT_NODE) {
                const hasImgTop = [child, ...child.querySelectorAll(".card-img-top")].some(
                    (node) =>
                        node.classList &&
                        node.classList.contains("card-img-top") &&
                        node.closest &&
                        node.closest(".card") === table
                );
                if (hasImgTop) {
                    // Collect .card-img-top superRows to manipulate their heights.
                    cardImgTopSuperRows.push(superRow);
                }
            }
        }
        // We expect successive .card-img-top to have the same height so the
        // bodies of the cards are aligned. This achieves that without flexboxes
        // by forcing the height of the smallest card:
        const smallestCardImgRow = Math.min(
            0,
            ...cardImgTopSuperRows.map((row) => row.clientHeight)
        );
        for (const row of cardImgTopSuperRows) {
            row.style.height = smallestCardImgRow + "px";
        }
        card.before(table);
        card.remove();
    }
}
/**
 * Convert CSS style to inline style (leave the classes on elements but forces
 * the style they give as inline style).
 *
 * @param {HTMLElement} $element
 * @param {Object} cssRules
 */
export function classToStyle(element, cssRules) {
    const writes = [];
    const nodeToRules = new Map();
    const rulesToProcess = [];
    for (const rule of cssRules) {
        const nodes = element.querySelectorAll(rule.selector);
        if (nodes.length) {
            rulesToProcess.push(rule);
        }
        for (const node of nodes) {
            const nodeRules = nodeToRules.get(node);
            if (!nodeRules) {
                nodeToRules.set(node, [rule]);
            } else {
                nodeRules.push(rule);
            }
        }
    }
    _computeStyleAndSpecificityOnRules(rulesToProcess);
    for (const rules of nodeToRules.values()) {
        rules.sort((a, b) => a.specificity - b.specificity);
    }

    for (const node of nodeToRules.keys()) {
        const nodeRules = nodeToRules.get(node);
        const css = nodeRules ? _getMatchedCSSRules(node, nodeRules) : {};
        // Flexbox
        for (const styleName of node.style) {
            if (styleName.includes("flex") || `${node.style[styleName]}`.includes("flex")) {
                writes.push(() => {
                    node.style[styleName] = "";
                });
            }
        }

        // Do not apply css that would override inline styles (which are prioritary).
        let style = node.getAttribute("style") || "";
        // Outlook doesn't support inline !important
        style = style.replace(/!important/g, "");
        for (const [key, value] of Object.entries(css)) {
            if (!new RegExp(`(^|;)\\s*${key}[ :]`).test(style)) {
                style = `${key}:${value};${style}`;
            }
        }
        style = correctBorderAttributes(style);
        if (Object.keys(style || {}).length === 0) {
            writes.push(() => {
                node.removeAttribute("style");
            });
        } else {
            writes.push(() => {
                node.setAttribute("style", style);
                if (node.style.width) {
                    node.setAttribute("width", node.style.width.replace("px", "").trim());
                }
            });
        }

        if (node.nodeName === "IMG") {
            writes.push(() => {
                // Media list images should not have an inline height
                if (node.classList.contains("s_media_list_img")) {
                    node.style.removeProperty("height");
                }
                // Protect aspect ratio when resizing in mobile.
                if (
                    node.style.getPropertyValue("width") === "100%" &&
                    node.style.getPropertyValue("object-fit") === ""
                ) {
                    node.style.setProperty("object-fit", "cover");
                }
            });
        }
        // Apple Mail
        if (node.nodeName === "TD" && !node.childNodes.length) {
            // Append non-breaking spaces to empty table cells.
            writes.push(() => {
                node.appendChild(document.createTextNode("\u00A0"));
            });
        }
        // Outlook
        if (
            node.nodeName === "A" &&
            node.classList.contains("btn") &&
            !node.classList.contains("btn-link") &&
            !node.children.length
        ) {
            writes.push(() => {
                node.before(
                    createMso(`<table align="center" border="0"
                    role="presentation" cellpadding="0" cellspacing="0"
                    style="border-radius: 6px; border-collapse: separate !important;">
                        <tbody>
                            <tr>
                                <td style="${node.style.cssText
                                    .replace(RE_PADDING_MATCH, "")
                                    .replaceAll('"', "&quot;")}" ${
                        node.parentElement.style.textAlign === "center" ? 'align="center" ' : ""
                    }bgcolor="${blendColors(node.style.backgroundColor)}">
                    `)
                );
                node.after(
                    createMso(`</td>
                        </tr>
                    </tbody>
                </table>`)
                );
            });
        } else if (
            node.nodeName === "IMG" &&
            node.classList.contains("mx-auto") &&
            node.classList.contains("d-block")
        ) {
            writes.push(() => {
                _wrap(node, "p", "o_outlook_hack", "text-align:center;margin:0");
            });
        }

        // Compute dynamic styles (var, calc).
        writes.push(() => {
            let computedStyle;
            for (const styleName of node.style) {
                const styleValue = node.style.getPropertyValue(styleName);
                if (styleValue.includes("var(") || styleValue.includes("calc(")) {
                    computedStyle = computedStyle || getComputedStyle(node);
                    const prop = styleValue.includes("var(")
                        ? styleValue.replace(/var\((.*)\)/, "$1")
                        : styleName;
                    const value =
                        computedStyle.getPropertyValue(prop) ||
                        computedStyle.getPropertyValue(styleName);
                    node.style.setProperty(styleName, value);
                }
            }
        });

        // Fix inheritance of font properties on Outlook.
        writes.push(() => {
            const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(
                (prop) => node.style[prop] === "inherit"
            );
            if (propsToConvert.length) {
                const computedStyle = getComputedStyle(node);
                for (const prop of propsToConvert) {
                    node.style.setProperty(prop, computedStyle[prop]);
                }
            }
        });

        const matchedBlacklistRules = nodeRules?.filter((rule) =>
            CONVERT_INLINE_BLACKLIST_CLASSES.some(
                (cls) => rule.selector.includes(cls) && node.classList.contains(cls)
            )
        );

        const blacklistedStyles = {};
        for (const rule of matchedBlacklistRules) {
            for (const [key, value] of Object.entries(rule.style)) {
                if (
                    !blacklistedStyles[key] ||
                    !blacklistedStyles[key].includes("important") ||
                    value.includes("important")
                ) {
                    blacklistedStyles[key] = value;
                }
            }
        }

        for (const [key, value] of Object.entries(blacklistedStyles)) {
            if (value && value.endsWith("important")) {
                blacklistedStyles[key] = value.replace(/\s*!important\s*$/, "");
            }
        }

        // Find styles to remove if they are from a blacklisted class and match
        // existing styles.
        const stylesToRemove = Object.fromEntries(
            Object.entries(css).filter(([key, value]) => blacklistedStyles[key] === value)
        );
        // Remove style from blacklisted classes.
        writes.push(() => {
            for (const [key] of Object.entries(stylesToRemove)) {
                if (node.style[key]) {
                    node.style.removeProperty(key);
                }
            }
        });
    }
    writes.forEach((fn) => fn());
}
/**
 * Add styles to all table rows and columns, that are necessary for them to be
 * responsive. This only works if columns have a max-width so the styles are
 * only applied to columns where that is the case.
 *
 * @param {Element} element
 */
function enforceTablesResponsivity(element) {
    // Trying this: https://www.litmus.com/blog/mobile-responsive-email-stacking/
    const trs = [...element.querySelectorAll(".o_mail_wrapper tr")]
        .filter((tr) => [...tr.children].some((td) => td.classList.contains("o_converted_col")))
        .reverse();
    for (const tr of trs) {
        const commonTable = _createTable();
        commonTable.style.height = "100%";
        const commonTr = document.createElement("tr");
        const commonTd = document.createElement("td");
        commonTr.appendChild(commonTd);
        commonTable.appendChild(commonTr);
        const tds = [...tr.children].filter((child) => child.nodeName === "TD");
        let index = 0;
        for (const td of tds) {
            const width = td.style.maxWidth;
            const div = document.createElement("div");
            div.style.display = "inline-block";
            div.style.verticalAlign = "top";
            div.classList.add("o_stacking_wrapper");
            commonTd.appendChild(div);
            const newTable = _createTable();
            newTable.style.width = width;
            newTable.classList.add("o_stacking_wrapper");
            div.appendChild(newTable);
            const newTr = document.createElement("tr");
            newTable.appendChild(newTr);
            newTr.appendChild(td);
            td.style.width = "100%";
            td.removeAttribute("width");
            if (index === 0) {
                div.before(
                    createMso(`
                    <table cellpadding="0" cellspacing="0" border="0" role="presentation" style="width: 100%;">
                        <tr>
                            <td valign="top" style="width: ${width};">`)
                );
            } else {
                div.before(createMso(`</td><td valign="top" style="width: ${width};">`));
            }
            if (index === tds.length - 1) {
                div.after(createMso(`</td></tr></table>`));
            }
            index++;
        }
        const topTd = document.createElement("td");
        topTd.appendChild(commonTable);
        tr.prepend(topTd);
    }
}
// Masonry has crazy nested tables that require some extra treatment.
function handleMasonry(element) {
    const masonryTrs = element.querySelectorAll(".s_masonry_block tr");
    for (const tr of masonryTrs) {
        const height = _getHeight(tr);
        const tds = [...tr.children].filter((child) => child.nodeName === "TD");
        const tdsWithTable = tds.filter((td) =>
            [...td.children].some((child) => child.nodeName === "TABLE")
        );
        if (tdsWithTable.length) {
            // TODO: this seems a duplicate of the other o_desktop_h100 set below.
            // Set the cells' heights to fill their parents.
            for (const tdWithTable of tdsWithTable) {
                tdWithTable.classList.add("o_desktop_h100");
                tdWithTable.style.setProperty("height", "100%");
            }
            // We also have to set the same height on the cells' sibling TDs.
            tds.forEach((td) => td.style.setProperty("height", height + "px"));
        }
        // Sometimes Masonry declares rows with a height of 100% but with
        // columns that overfit the grid. In these cases, we split the rows into
        // multiple rows so we need to adapt their heights for them to be
        // divided equally.
        const trSiblings = [...tr.parentElement.children].filter(
            (child) => child.nodeName === "TR"
        );
        if (
            trSiblings.length > 1 &&
            (tr.classList.contains("h-100") || tr.style.getPropertyValue("height") === "100%")
        ) {
            tr.style.setProperty("height", `${_getHeight(tr.parentElement) / trSiblings.length}px`);
        }
    }
    for (const tr of masonryTrs) {
        const height = tr.style.height.includes("px")
            ? parseFloat(tr.style.height.replace("px", "").trim())
            : _getHeight(tr);
        tr.closest("table").classList.add("o_desktop_h100");
        tr.classList.add("o_desktop_h100");
        for (const td of [...tr.children].filter((child) => child.nodeName === "TD")) {
            td.classList.add("o_desktop_h100");
            td.style.setProperty("height", "100%");
            const childrenNames = [...td.children].map((child) => child.nodeName);
            if (!childrenNames.includes("TABLE")) {
                // Hack that makes vertical-align possible within an inline-block.
                const wrapper = document.createElement("div");
                wrapper.style.setProperty("display", "inline-block");
                wrapper.style.setProperty("width", "100%");
                // Transfer color to wrapper for Outlook on MacOS/iOS.
                const tdStyle = getComputedStyle(td);
                wrapper.style.setProperty("color", tdStyle.color);
                const firstNonCommentChild = [...td.childNodes].find(
                    (child) => child.nodeType !== Node.COMMENT_NODE
                );
                let anchor;
                if (firstNonCommentChild) {
                    anchor = getAdjacentPreviousSiblings(firstNonCommentChild)
                        .filter((sib) => sib.nodeType !== Node.TEXT_NODE)
                        .shift();
                }
                for (const child of [...td.childNodes].filter(
                    (child) => child.nodeType !== Node.COMMENT_NODE
                )) {
                    wrapper.append(child);
                }
                anchor ? anchor.after(wrapper) : td.append(wrapper);
                const centeringSpan = document.createElement("span");
                centeringSpan.style.setProperty("height", "100%");
                centeringSpan.style.setProperty("display", "inline-block");
                centeringSpan.style.setProperty("vertical-align", "middle");
                td.prepend(centeringSpan);
                // Height on cells should be applied in pixels.
                if (td.style.height.includes("%")) {
                    const newHeight =
                        (height * parseFloat(td.style.height.replace("%").trim())) / 100;
                    td.style.setProperty("height", newHeight + "px");
                    // Spread height down for responsivity
                    td.style.setProperty("max-height", newHeight + "px");
                    wrapper.style.setProperty("max-height", newHeight + "px");
                    if (
                        wrapper.childElementCount === 1 &&
                        wrapper.firstElementChild.nodeName === "IMG" &&
                        wrapper.firstElementChild.style.height === "100%"
                    ) {
                        wrapper.firstElementChild.style.setProperty("max-height", newHeight + "px");
                    }
                }
            }
        }
    }
}
/**
 * Modify the styles of images so they are responsive.
 *
 * @param {Element} element
 */
function enforceImagesResponsivity(element) {
    // Images with 100% height in cells should preserve that height and the
    // height of the row should be applied to the cell.
    for (const image of element.querySelectorAll("td > img")) {
        const td = image.parentElement;
        if (
            td.childElementCount === 1 &&
            (image.classList.contains("h-100") ||
                _getStylePropertyValue(image, "height") === "100%")
        ) {
            td.style.setProperty("height", _getHeight(td.parentElement) + "px");
            image.style.setProperty("height", "100%");
        }
    }
    // Remove the height attribute in card images so they can resize
    // responsively, but leave it for Outlook.
    for (const image of element.querySelectorAll('img[width="100%"][height]')) {
        image.before(createMso(image.outerHTML));
        image.classList.add("mso-hide");
        image.removeAttribute("height");
    }
}
/**
 * Convert the contents of an element area into content
 * that is widely compatible with email clients.
 *
 * @param {HTMLElement} element
 * @param {Object[]} cssRules Array<{selector: string;
 *                            style: {[styleName]: string};
 *                            specificity: number;}>
 */
export async function toInline(element, cssRules) {
    // Fix card-img-top heights (must happen before we transform everything).
    for (const imgTop of element.querySelectorAll(".card-img-top")) {
        imgTop.style.setProperty("height", _getHeight(imgTop) + "px");
    }

    // Fix empty element heights to be always visible as they might have borders
    // (used as separation) and can be rendered with height 0px.
    // like having empty div with % height and display inline-block.
    for (const el of element.querySelectorAll(".o_not_editable[class*='border-']:empty")) {
        el.style.height = getComputedStyle(el).height;
    }

    attachmentThumbnailToLinkImg(element);
    fontToImg(element);
    await svgToPng(element);

    // Fix img-fluid for Outlook.
    for (const image of element.querySelectorAll("img.img-fluid")) {
        const width = _getWidth(image);
        const clone = image.cloneNode();
        clone.setAttribute("width", width);
        clone.style.setProperty("width", width + "px");
        clone.style.removeProperty("max-width");
        image.before(createMso(clone.outerHTML));
        _hideForOutlook(image);
    }

    classToStyle(element, cssRules);
    bootstrapToTable(element);
    cardToTable(element);
    listGroupToTable(element);
    addTables(element);
    handleMasonry(element);
    const rootFontSizeProperty = getComputedStyle(element.ownerDocument.documentElement).fontSize;
    const rootFontSize = parseFloat(rootFontSizeProperty.replace(/[^\d.]/g, ""));
    normalizeRem(element, rootFontSize);
    enforceImagesResponsivity(element);
    enforceTablesResponsivity(element);
    flattenBackgroundImages(element);
    formatTables(element);
    normalizeColors(element);
    responsiveToStaticForOutlook(element);
    // Fix Outlook image rendering bug.
    for (const attributeName of ["width", "height"]) {
        const images = element.querySelectorAll("img");
        for (const image of images) {
            if (image.style[attributeName] !== "auto") {
                const value =
                    image.getAttribute(attributeName) ||
                    (attributeName === "height" && image.offsetHeight) ||
                    (attributeName === "width" ? _getWidth(image) : _getHeight(image));
                if (value) {
                    image.setAttribute(attributeName, value);
                    image.style.setProperty(attributeName, value + "px");
                }
            }
        }
    }
    // Fix mx-auto on images in table cells.
    for (const centeredImage of element.querySelectorAll("td > img.mx-auto")) {
        if (centeredImage.parentElement.children.length === 1) {
            centeredImage.parentElement.style.setProperty("text-align", "center");
        }
    }

    // Remove contenteditable attributes
    [element, ...element.querySelectorAll("[contenteditable]")].forEach((node) =>
        node.removeAttribute("contenteditable")
    );

    // Hide replaced cells on Outlook
    element.querySelectorAll(".mso-hide").forEach(_hideForOutlook);

    // Replace double quotes in font-family styles with simple quotes (and
    // simply remove these styles from images).
    element
        .querySelectorAll("[style*=font-family]")
        .forEach((n) =>
            n.nodeName === "IMG"
                ? n.style.removeProperty("font-family")
                : n.setAttribute("style", n.getAttribute("style").replaceAll('"', "'"))
        );

    element
        .querySelectorAll(".o_converted_col")
        .forEach((node) => node.classList.remove("o_converted_col"));
}
/**
 * Take all elements with a `background-image` style and convert them to `vml`
 * for Outlook. Also remove data-bg-src to avoid Gmail cutting the html.
 *
 * @param {Element} element
 */
function flattenBackgroundImages(element) {
    const backgroundImages = [...element.querySelectorAll("*[style*=background-image]")]
        .filter((el) => !el.closest(".mso-hide"))
        .reverse();
    for (const backgroundImage of backgroundImages) {
        const vml = _backgroundImageToVml(backgroundImage);
        if (vml) {
            // Put the Outlook version after the original one in an mso conditional.
            backgroundImage.after(createMso(vml));
            // Hide the original element for Outlook.
            backgroundImage.classList.add("mso-hide");
        }
        if (backgroundImage.hasAttribute("data-bg-src")) {
            // Remove data-bg-src as it is not needed for email rendering and
            // can cause Gmail to cut the email prematurely if the attributes
            // contain an image in the form of a long base64 string.
            backgroundImage.removeAttribute("data-bg-src");
        }
    }
}
/**
 * Convert font icons to images.
 *
 * @param {HTMLElement} element - the element in which the font icons have to be
 *                           converted to images
 */
function fontToImg(element) {
    const { fonts } = odoo.loader.modules.get("@html_editor/utils/fonts");

    for (const font of element.querySelectorAll(".fa")) {
        let icon, content;
        fonts.fontIcons.find((fontIcon) =>
            fonts.getCssSelectors(fontIcon.parser).find((data) => {
                if (font.matches(data.selector.replace(/::?before/g, ""))) {
                    icon = data.names[0].split("-").shift();
                    content = data.css.match(/content:\s*['"]?(.)['"]?/)[1];
                    return true;
                }
            })
        );
        if (content) {
            const color = _getStylePropertyValue(font, "color").replace(/\s/g, "");
            let backgroundColoredElement = font;
            let bg, isTransparent;
            do {
                bg = _getStylePropertyValue(backgroundColoredElement, "background-color").replace(
                    /\s/g,
                    ""
                );
                isTransparent = bg === "transparent" || bg === "rgba(0,0,0,0)";
                backgroundColoredElement = backgroundColoredElement.parentElement;
            } while (isTransparent && backgroundColoredElement);
            if (bg === "rgba(0,0,0,0)" && isTransparent) {
                // default on white rather than black background since opacity
                // is not supported.
                bg = "rgb(255,255,255)";
            }
            const style = font.getAttribute("style");
            const width = _getWidth(font);
            const height = _getHeight(font);
            const lineHeight = _getStylePropertyValue(font, "line-height");
            // Compute the padding.
            // First get the dimensions of the icon itself (::before)
            font.style.setProperty("height", "fit-content");
            font.style.setProperty("width", "fit-content");
            font.style.setProperty("line-height", "normal");
            const intrinsicWidth = _getWidth(font);
            const intrinsicHeight = _getHeight(font);
            const hPadding = width && intrinsicWidth && (width - intrinsicWidth) / 2;
            const vPadding = height && intrinsicHeight && (height - intrinsicHeight) / 2;
            let padding = "";
            if (hPadding || vPadding) {
                padding = vPadding ? vPadding + "px " : "0 ";
                padding += hPadding ? hPadding + "px" : "0";
            }
            const image = document.createElement("img");
            image.setAttribute("width", intrinsicWidth);
            image.setAttribute("height", intrinsicHeight);
            image.setAttribute(
                "src",
                `/mail/font_to_img/${content.charCodeAt(0)}/${encodeURIComponent(
                    color
                )}/${encodeURIComponent(bg)}/${Math.max(1, Math.round(intrinsicWidth))}x${Math.max(
                    1,
                    Math.round(intrinsicHeight)
                )}`
            );
            image.setAttribute("data-class", font.getAttribute("class"));
            image.setAttribute("data-style", style);
            image.setAttribute("style", style);
            image.style.setProperty("box-sizing", "border-box"); // keep the fontawesome's dimensions
            image.style.setProperty("line-height", lineHeight);
            image.style.setProperty("width", intrinsicWidth + "px");
            image.style.setProperty("height", intrinsicHeight + "px");
            image.style.setProperty("vertical-align", "unset"); // undo Bootstrap's default (middle).
            if (!padding) {
                image.style.setProperty("margin", _getStylePropertyValue(font, "margin"));
            }
            // For rounded images, apply the rounded border to a wrapper, make
            // sure it doesn't get applied to the image itself so the image
            // doesn't get cropped in the process.
            const wrapper = document.createElement("span");
            wrapper.style.setProperty("display", "inline-block");
            wrapper.append(image);
            font.before(wrapper);
            if (font.classList.contains("mx-auto")) {
                wrapper.parentElement.style.textAlign = "center";
            }
            font.remove();
            wrapper.style.setProperty("padding", padding);
            const wrapperWidth =
                width +
                ["left", "right"].reduce(
                    (sum, side) =>
                        sum +
                        (+_getStylePropertyValue(image, `margin-${side}`).replace("px", "") || 0),
                    0
                );
            wrapper.style.setProperty("width", wrapperWidth + "px");
            wrapper.style.setProperty("height", height + "px");
            wrapper.style.setProperty("vertical-align", "text-bottom");
            wrapper.style.setProperty("background-color", image.style.backgroundColor);
            wrapper.setAttribute(
                "class",
                "oe_unbreakable " + // prevent sanitize from grouping image wrappers
                    font
                        .getAttribute("class")
                        .replace(new RegExp("(^|\\s+)" + icon + "(-[^\\s]+)?", "gi"), "") // remove inline font-awsome style
            );
        } else {
            font.remove();
        }
    }
}
/**
 * Format table styles so they display well in most mail clients. This implies
 * moving table paddings to its cells, adding tbody (with canceled styles) where
 * needed, and adding pixel heights to parents of elements with percent heights.
 *
 * @param {HTMLElement} element
 */
export function formatTables(element) {
    const writes = [];
    for (const table of element.querySelectorAll(
        "table.o_mail_snippet_general, .o_mail_snippet_general table"
    )) {
        const tablePaddingTop = parseFloat(
            _getStylePropertyValue(table, "padding-top").match(RE_PADDING)[1]
        );
        const tablePaddingRight = parseFloat(
            _getStylePropertyValue(table, "padding-right").match(RE_PADDING)[1]
        );
        const tablePaddingBottom = parseFloat(
            _getStylePropertyValue(table, "padding-bottom").match(RE_PADDING)[1]
        );
        const tablePaddingLeft = parseFloat(
            _getStylePropertyValue(table, "padding-left").match(RE_PADDING)[1]
        );
        const rows = [...table.querySelectorAll("tr")].filter(
            (tr) => tr.closest("table") === table
        );
        const columns = [...table.querySelectorAll("td")].filter(
            (td) => td.closest("table") === table
        );
        for (const column of columns) {
            const columnsInRow = [...column.closest("tr").querySelectorAll("td")].filter(
                (td) => td.closest("table") === table
            );
            const columnIndex = columnsInRow.findIndex((col) => col === column);
            const rowIndex = rows.findIndex((row) => row === column.closest("tr"));

            if (!rowIndex) {
                const match = _getStylePropertyValue(column, "padding-top").match(RE_PADDING);
                const columnPaddingTop = match ? parseFloat(match[1]) : 0;
                writes.push(() => {
                    column.style["padding-top"] = `${columnPaddingTop + tablePaddingTop}px`;
                });
            }
            if (columnIndex === columnsInRow.length - 1) {
                const match = _getStylePropertyValue(column, "padding-right").match(RE_PADDING);
                const columnPaddingRight = match ? parseFloat(match[1]) : 0;
                writes.push(() => {
                    column.style["padding-right"] = `${columnPaddingRight + tablePaddingRight}px`;
                });
            }
            if (rowIndex === rows.length - 1) {
                const match = _getStylePropertyValue(column, "padding-bottom").match(RE_PADDING);
                const columnPaddingBottom = match ? parseFloat(match[1]) : 0;
                writes.push(() => {
                    column.style["padding-bottom"] = `${
                        columnPaddingBottom + tablePaddingBottom
                    }px`;
                });
            }
            if (!columnIndex) {
                const match = _getStylePropertyValue(column, "padding-left").match(RE_PADDING);
                const columnPaddingLeft = match ? parseFloat(match[1]) : 0;
                writes.push(() => {
                    column.style["padding-left"] = `${columnPaddingLeft + tablePaddingLeft}px`;
                });
            }
        }
        writes.push(() => {
            table.style.removeProperty("padding");
        });
    }
    writes.forEach((fn) => fn());
    // Ensure a tbody in every table and cancel its default style.
    for (const table of [...element.querySelectorAll("table")].filter(
        (n) => ![...n.children].some((c) => c.nodeName === "TBODY")
    )) {
        const contents = [...table.childNodes];
        const tbody = document.createElement("tbody");
        tbody.style.setProperty("vertical-align", "top");
        table.prepend(tbody);
        tbody.append(...contents);
    }
    // Children will only take 100% height if the parent has a height property.
    for (const node of [...element.querySelectorAll("*")].filter(
        (n) =>
            n.style &&
            n.style.getPropertyValue("height") === "100%" &&
            (!n.parentElement.style.getPropertyValue("height") ||
                n.parentElement.style.getPropertyValue("height").includes("%"))
    )) {
        let parent = node.parentElement;
        let height = parent.style.getPropertyValue("height");
        while (parent && height && height.includes("%")) {
            parent = parent.parentElement;
            height = parent.style.getPropertyValue("height");
        }
        if (parent) {
            parent.style.setProperty("height", parent.getBoundingClientRect().height);
        }
    }
    // Align self and justify content don't work on table cells.
    for (const cell of element.querySelectorAll("td")) {
        const alignSelf = cell.style.alignSelf;
        const justifyContent = cell.style.justifyContent;
        if (
            alignSelf === "start" ||
            justifyContent === "start" ||
            justifyContent === "flex-start"
        ) {
            cell.style.verticalAlign = "top";
        } else if (alignSelf === "center" || justifyContent === "center") {
            const parentCell = cell.parentElement.closest("td");
            const parentTable = cell.closest("table");
            if (parentCell) {
                parentTable.style.height = _getHeight(parentCell) + "px";
            }
            cell.style.verticalAlign = "middle";
        } else if (
            alignSelf === "end" ||
            justifyContent === "end" ||
            justifyContent === "flex-end"
        ) {
            cell.style.verticalAlign = "bottom";
        }
    }
    // Align items doesn't work on table rows.
    for (const row of element.querySelectorAll("tr")) {
        const alignItems = row.style.alignItems;
        if (alignItems === "flex-start") {
            row.style.verticalAlign = "top";
        } else if (alignItems === "center") {
            row.style.verticalAlign = "middle";
        } else if (alignItems === "flex-end" || alignItems === "baseline") {
            row.style.verticalAlign = "bottom";
        } else if (alignItems === "stretch") {
            const columns = [...row.querySelectorAll("td.o_converted_col")];
            if (columns.length > 1) {
                const commonAncestor = commonParentGet(columns[0], columns[1]);
                const biggestHeight = commonAncestor.clientHeight;
                for (const column of columns) {
                    column.style.height = biggestHeight + "px";
                }
            }
        }
    }
    // Tables don't properly inherit certain styles from their ancestors in Outlook.
    for (const table of element.querySelectorAll("table")) {
        const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(
            (prop) => table.style[prop] === "inherit" || !table.style[prop]
        );
        if (propsToConvert.length) {
            for (const prop of propsToConvert) {
                let ancestor = table;
                while (ancestor && (!ancestor.style[prop] || ancestor.style[prop] === "inherit")) {
                    ancestor = ancestor.parentElement;
                }
                if (ancestor) {
                    table.style.setProperty(prop, ancestor.style[prop]);
                }
            }
        }
    }
}
/**
 * Parse through the given document's stylesheets, preprocess(*) them and return
 * the result as an array of objects, each containing a selector string , a
 * style object and a specificity number. Preprocessing involves grouping
 * whatever rules can be grouped together and precomputing their specificity so
 * as to sort them appropriately.
 *
 * @param {Document} doc
 * @returns {Object[]} Array<{selector: string;
 *                            style: {[styleName]: string};
 *                            specificity: number;}>
 */
export function getCSSRules(doc) {
    const cssRules = [];
    for (const sheet of [...doc.styleSheets, ...doc.adoptedStyleSheets]) {
        // try...catch because browser may not able to enumerate rules for cross-domain sheets
        let rules;
        try {
            rules = sheet.rules || sheet.cssRules;
        } catch (e) {
            console.log("Can't read the css rules of: " + sheet.href, e);
            continue;
        }
        for (const rule of rules || []) {
            const subRules = [rule];
            const conditionText = rule.conditionText;
            const minWidthMatch = conditionText && conditionText.match(/\(min-width *: *(\d+)/);
            const minWidth = minWidthMatch && +(minWidthMatch[1] || "0");
            if (minWidth && minWidth >= 768) {
                // Large min-width media queries should be included.
                // eg., .container has a default max-width for all screens.
                let mediaRules;
                try {
                    mediaRules = rule.rules || rule.cssRules;
                    subRules.push(...mediaRules);
                } catch (e) {
                    console.log(`Can't read the css rules of: ${sheet.href} (${conditionText})`, e);
                }
            }
            for (const subRule of subRules) {
                const selectorText = subRule.selectorText || "";
                // Split selectors, making sure not to split at commas in parentheses.
                for (const selector of splitSelectorAroundCommasOutsideParentheses(selectorText)) {
                    if (selector && !SELECTORS_IGNORE.test(selector)) {
                        cssRules.push({ selector: selector.trim(), rawRule: subRule });
                        if (selector === "body") {
                            // The top element of a mailing has the class
                            // 'o_layout'. Give it the body's styles so they can
                            // trickle down.
                            cssRules.push({
                                selector: ".o_layout",
                                rawRule: subRule,
                                specificity: 1,
                            });
                        }
                    }
                }
            }
        }
    }

    return cssRules;
}
/**
 * Convert Bootstrap list groups and their items to table structures.
 *
 * @param {Element} element
 */
export function listGroupToTable(element) {
    for (const listGroup of element.querySelectorAll(".list-group")) {
        let table;
        if (listGroup.querySelectorAll(".list-group-item").length) {
            table = _createTable(listGroup.attributes);
        } else {
            table = listGroup.cloneNode();
            for (const attr of listGroup.attributes) {
                table.setAttribute(attr.name, attr.value);
            }
        }
        for (const child of [...listGroup.childNodes]) {
            if (child.classList && child.classList.contains("list-group-item")) {
                // List groups are <ul>s that render like tables. Their
                // li.list-group-item children should translate to tr > td.
                const row = document.createElement("tr");
                const col = document.createElement("td");
                for (const attr of child.attributes) {
                    col.setAttribute(attr.name, attr.value);
                }
                col.append(...child.childNodes);
                col.classList.remove("list-group-item");
                if (!col.className) {
                    col.removeAttribute("class");
                }
                row.append(col);
                table.append(row);
                child.remove();
            } else if (child.nodeName === "LI") {
                table.append(...child.childNodes);
            } else {
                table.append(child);
            }
        }
        table.classList.remove("list-group");
        if (!table.className) {
            table.removeAttribute("class");
        }
        if (listGroup.nodeName === "TD") {
            listGroup.append(table);
            listGroup.classList.remove("list-group");
            if (!listGroup.className) {
                listGroup.removeAttribute("class");
            }
        } else {
            listGroup.before(table);
            listGroup.remove();
        }
    }
}
/**
 * Convert all styles containing rgb colors to hexadecimal colors.
 * Note: ignores rgba colors, which are not supported in Microsoft Outlook.
 *
 * @param {HTMLElement} element
 */
export function normalizeColors(element) {
    for (const node of element.querySelectorAll('[style*="rgb"]')) {
        const rgbaMatch = node.getAttribute("style").match(/rgba?\(([\d.]+\s*,?\s*){3,4}\)/g);
        for (const rgb of rgbaMatch || []) {
            node.setAttribute(
                "style",
                node.getAttribute("style").replace(rgb, blendColors(rgb, node))
            );
        }
    }
}
/**
 * Convert all css values that use the rem unit to px.
 *
 * @param {HTMLElement} $element
 * @param {Number} rootFontSize=16 The font size of the root element, in pixels
 */
export function normalizeRem(element, rootFontSize = 16) {
    for (const node of element.querySelectorAll('[style*="rem"]')) {
        const remMatch = node.getAttribute("style").match(/[\d.]+\s*rem/g);
        for (const rem of remMatch || []) {
            const remValue = parseFloat(rem.replace(/[^\d.]/g, ""));
            const pxValue = Math.round(remValue * rootFontSize * 100) / 100;
            node.setAttribute("style", node.getAttribute("style").replace(rem, pxValue + "px"));
        }
    }
}

/**
 * This replaces column html with a dumbed down, Outlook-compliant version of
 * them just for Outlook so while not responsive, these columns still display OK
 * on Outlook.
 *
 * @param {Element} element
 */
function responsiveToStaticForOutlook(element) {
    // Replace the responsive tables with static ones for Outlook
    for (const td of element.querySelectorAll("td.o_converted_col:not(.mso-hide)")) {
        const tdStyle = td.getAttribute("style") || "";
        const msoAttributes = [...td.attributes].filter(
            (attr) => attr.name !== "style" && attr.name !== "width"
        );
        const msoWidth = td.style.getPropertyValue("max-width");
        const msoStyles = tdStyle.replace(/(^| |max-)width:[^;]*;\s*/g, "");
        const outlookTd = document.createElement("td");
        for (const attribute of msoAttributes) {
            outlookTd.setAttribute(attribute.name, td.getAttribute(attribute.name));
        }
        if (msoWidth) {
            outlookTd.setAttribute("width", ("" + msoWidth).replace("px", "").trim());
            outlookTd.setAttribute("style", `${msoStyles}width: ${msoWidth};`);
        } else {
            outlookTd.setAttribute("style", msoStyles);
        }
        if (td.closest(".s_masonry_block")) {
            outlookTd.style.padding = 0; // Not sure why this is needed.
        }
        // Outlook doesn't support left/right padding on images. When the image
        // is the only child of its parent, apply said padding to the parent.
        if (td.children.length === 1 && td.firstElementChild.nodeName === "IMG") {
            const tdComputedStyle = getComputedStyle(td);
            for (const side of ["left", "right"]) {
                if (td.firstElementChild.style.width === "100%") {
                    const prop = `padding-${side}`;
                    const imagePadding = +td.firstElementChild.style[prop].replace("px", "");
                    if (imagePadding > 0) {
                        const tdPadding = +tdComputedStyle[prop].replace("px", "") || 0;
                        outlookTd.style[prop] = tdPadding + imagePadding + "px";
                    }
                }
            }
        }
        // The opening tag of `outlookTd` is for Outlook.
        td.before(createMso(outlookTd.outerHTML.replace("</td>", "")));
        // The opening tag of `td` is for the others.
        _hideForOutlook(td, "opening");
    }
}
/**
 * Convert images of type svg to png.
 *
 * @param {HTMLElement} element
 */
async function svgToPng(element) {
    for (const svg of element.querySelectorAll('img[src*=".svg"]')) {
        // Make sure the svg is loaded before we convert it.
        await new Promise((resolve) => {
            svg.onload = () => resolve();
            if (svg.complete) {
                resolve();
            }
        });
        const image = document.createElement("img");
        const canvas = document.createElement("CANVAS");
        const width = _getWidth(svg);
        const height = _getHeight(svg);

        canvas.setAttribute("width", width);
        canvas.setAttribute("height", height);
        canvas.getContext("2d").drawImage(svg, 0, 0, width, height);

        for (const attribute of svg.attributes) {
            image.setAttribute(attribute.name, attribute.value);
        }

        image.setAttribute("src", canvas.toDataURL("png"));
        image.setAttribute("width", width);
        image.setAttribute("height", height);

        svg.before(image);
        svg.remove();
    }
}

//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------

/**
 * Take an element and apply a colspan to it. In this context, this implies to
 * also apply a width to it, that corresponds to the colspan.
 *
 * @param {Element} element
 * @param {number} colspan
 * @param {number} tableWidth
 */
function _applyColspan(element, colspan, tableWidth) {
    element.setAttribute("colspan", colspan);
    const widthPercentage = +element.getAttribute("colspan") / 12;
    // Round to 2 decimal places.
    const width = Math.round(tableWidth * widthPercentage * 100) / 100;
    element.style.setProperty("max-width", width + "px");
    element.classList.add("o_converted_col");
}
/**
 * Take an element with a background image and return a string containing the
 * VML code to display the same image properly in Outlook, with its contents
 * inside.
 * Note that this assumes:
 *   - background-size: cover,
 *   - background-repeat: no-repeat,
 *   - size 100%
 *   - content is centered x/y
 * TODO: centering span probably not needed with `v-text-anchor:middle` present.
 *
 * @param {Element} backgroundImage
 * @returns {string}
 */
function _backgroundImageToVml(backgroundImage) {
    const matches = backgroundImage.style.backgroundImage.match(/url\("?(.+?)"?\)/);
    const url = matches && matches[1];
    if (url) {
        // Create the outer structure.
        const clone = backgroundImage.cloneNode(true);
        const div = document.createElement("div");
        div.replaceChildren(...clone.childNodes);
        [
            ["fontSize", 0],
            ["height", "100%"],
            ["width", "100%"],
        ].forEach(([k, v]) => (div.style[k] = v));
        const vmlContent = document.createElement("div");
        vmlContent.append(div);

        // Preserve important inherited properties without ancestor context.
        const style = getComputedStyle(backgroundImage);
        for (const prop of FONT_PROPERTIES_TO_INHERIT) {
            div.style[prop] = backgroundImage.style[prop] || style[prop];
        }
        [...div.children].forEach((child) =>
            child.style.setProperty("font-size", child.style.fontSize || style.fontSize)
        );

        // Prepare the top element for hosting the VML image.
        for (const prop of [
            "background",
            "background-image",
            "background-repeat",
            "background-size",
        ]) {
            clone.style.removeProperty(prop);
        }
        clone.style.padding = 0;
        clone.className = clone.className.replace(/p[bt]\d+/g, ""); // Remove padding classes.
        clone.setAttribute("background", url);
        clone.setAttribute("valign", "middle");

        // Create the VML structure, with the content of the original element inside.
        const [width, height] = [_getWidth(backgroundImage), _getHeight(backgroundImage)];
        const vml =
            `<v:image xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" ` +
            `style="border: 0; display: inline-block; width: ${width}px; height: ${height}px;" src="${url}"/>
        <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" ` +
            `style="border: 0; display: inline-block; position: absolute; width:${width}px; height:${height}px; v-text-anchor:middle;">
            <v:fill opacity="0%" color="#000000"/>
            <v:textbox inset="0,0,0,0">
                <table border="0" cellpadding="0" cellspacing="0">
                    <tr>
                        <td width="${width}" align="center" style="text-align: center;">${vmlContent.outerHTML}</td>
                    </tr>
                </table>
            </v:textbox>
        </v:rect>`;

        // Wrap the VML in the original opening and closing tags.
        return `${clone.outerHTML.replace(
            /<\/[\w-]+>[\s\n]*$/,
            ""
        )}${vml}</${clone.nodeName.toLowerCase()}>`;
    }
}
/**
 * Take a selector and return its specificity according to the w3 specification.
 *
 * @see http://www.w3.org/TR/css3-selectors/#specificity
 * @param {string} selector
 * @returns number
 */
function _computeSpecificity(selector) {
    let a = 0;
    selector = selector.replace(/#[a-z0-9_-]+/gi, () => {
        a++;
        return "";
    });
    let b = 0;
    selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, () => {
        b++;
        return "";
    });
    let c = 0;
    selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, (a) => {
        if (!a.includes(":not(")) {
            c++;
        }
        return "";
    });
    return a * 100 + b * 10 + c;
}
/**
 * Take all the rules and modify them to contain information on their
 * specificity and to have normalized style.
 *
 * @see _computeSpecificity
 * @see _normalizeStyle
 * @param {Object} cssRules
 */
function _computeStyleAndSpecificityOnRules(cssRules) {
    for (const cssRule of cssRules) {
        if (!cssRule.style && cssRule.rawRule.style) {
            const style = _normalizeStyle(cssRule.rawRule.style);
            if (Object.keys(style).length) {
                Object.assign(cssRule, {
                    style,
                    specificity: _computeSpecificity(cssRule.selector),
                });
            } else {
                Object.assign(cssRule, {
                    specificity: 0,
                });
            }
        }
    }
}
/**
 * Return an array of twelve table cells as JQuery elements.
 *
 * @returns {Element[]}
 */
function _createColumnGrid() {
    return new Array(12).fill().map(() => document.createElement("td"));
}
/**
 * Return a comment element with the given content, wrapped in an mso condition.
 *
 * @param {string} content
 * @returns {Comment}
 */
export function createMso(content = "") {
    // We remove comments having opposite condition from the one we will insert
    // We remove comment tags having the same condition
    const showRegex = /<!--\[if\s+mso\]>([\s\S]*?)<!\[endif\]-->/g;
    const hideRegex = /<!--\[if\s+!mso\]>([\s\S]*?)<!\[endif\]-->/g;
    let contentToInsert = content;
    contentToInsert = contentToInsert.replace(showRegex, (matchedContent, group) => group);
    contentToInsert = contentToInsert.replace(hideRegex, "");
    return document.createComment(`[if mso]>${contentToInsert}<![endif]`);
}
/**
 * Return a table element, with its default styles and attributes, as well as
 * the applicable given attributes, if any.
 *
 * @see TABLE_ATTRIBUTES
 * @see TABLE_STYLES
 * @param {NamedNodeMap | Attr[]} [attributes] default: []
 * @returns {Element}
 */
function _createTable(attributes = []) {
    const table = document.createElement("table");
    Object.entries(TABLE_ATTRIBUTES).forEach(([att, value]) => table.setAttribute(att, value));
    for (const attr of attributes) {
        if (!(attr.name === "width" && attr.value === "100%")) {
            table.setAttribute(attr.name, attr.value);
        }
    }
    table.style.setProperty("width", "100%", "important");
    if (table.classList.contains("o_layout")) {
        // The top mailing element inherits the body's font size and line-height
        // and should keep them.
        const layoutStyles = { ...TABLE_STYLES };
        delete layoutStyles["font-size"];
        delete layoutStyles["line-height"];
        Object.entries(layoutStyles).forEach(([att, value]) => (table.style[att] = value));
    } else {
        for (const styleName in TABLE_STYLES) {
            if (!("style" in attributes && attributes.style.value.includes(styleName + ":"))) {
                table.style[styleName] = TABLE_STYLES[styleName];
            }
        }
    }
    return table;
}
/**
 * Take a Bootstrap grid column element and return its size, computed by using
 * its Bootstrap classes.
 *
 * @see RE_COL_MATCH
 * @param {Element} column
 * @returns {number}
 */
function _getColumnSize(column) {
    const colMatch = column.className.match(RE_COL_MATCH);
    const colOptions = colMatch[2] && colMatch[2].substr(1).split("-");
    const colSize =
        (colOptions && (colOptions.length === 2 ? +colOptions[1] : +colOptions[0])) || 0;
    return colSize;
}
/**
 * Take a Bootstrap grid column element and return its offset size, computed by
 * using its Bootstrap classes.
 *
 * @see RE_OFFSET_MATCH
 * @param {Element} column
 * @returns {number}
 */
function _getColumnOffsetSize(column) {
    const offsetMatch = column.className.match(RE_OFFSET_MATCH);
    const offsetOptions = offsetMatch && offsetMatch[2] && offsetMatch[2].substr(1).split("-");
    const offsetSize =
        (offsetOptions && (offsetOptions.length === 2 ? +offsetOptions[1] : +offsetOptions[0])) ||
        0;
    return offsetSize;
}
/**
 * Return the CSS rules which applies on an element, tweaked so that they are
 * browser/mail client ok.
 *
 * @param {Node} node
 * @param {Object[]} Array<{selector: string;
 *                          style: {[styleName]: string};
 *                          specificity: number;}>
 * @returns {Object} {[styleName]: string}
 */
function _getMatchedCSSRules(node, cssRules) {
    node.matches =
        node.matches ||
        node.webkitMatchesSelector ||
        node.mozMatchesSelector ||
        node.msMatchesSelector ||
        node.oMatchesSelector;

    const styles = cssRules.map((rule) => removeBlacklistedStyles(rule, node)).filter(Boolean);

    // Add inline styles at the highest specificity.
    if (node.style.length) {
        const inlineStyles = {};
        for (const styleName of node.style) {
            inlineStyles[styleName] = node.style[styleName];
        }
        styles.push(inlineStyles);
    }

    const processedStyle = {};
    for (const style of styles) {
        for (const [key, value] of Object.entries(style)) {
            if (
                !processedStyle[key] ||
                !processedStyle[key].includes("important") ||
                value.includes("important")
            ) {
                processedStyle[key] = value;
            }
        }
    }

    for (const [key, value] of Object.entries(processedStyle)) {
        if (value && value.endsWith("important")) {
            processedStyle[key] = value.replace(/\s*!important\s*$/, "");
        }
    }

    // When a grouped style (e.g., border-width, margin, padding) uses a CSS variable
    // (e.g., var(--some-variable)), its substyles (e.g., margin-left, padding-top)
    // won't have explicit values in CSSRule's style property. The grouped style itself
    // also won't appear directly. To prevent losing these styles, we add the substyles
    // explicitly using their computed values.
    const computedStyle = getComputedStyle(node);
    for (const groupName in GROUPED_STYLES) {
        // We exclude the 'margin' and 'padding' styles from force apply because
        // it's common that they have a value set by auto which doesn't make sense to
        // force their computed value.
        const force = !groupName.includes("margin") && !groupName.includes("padding");
        const hasSubStyleApplied = GROUPED_STYLES[groupName].some(
            (styleName) => styleName in processedStyle
        );
        if (!force && hasSubStyleApplied) {
            continue;
        }
        for (const styleName of GROUPED_STYLES[groupName]) {
            const styleValue = computedStyle.getPropertyValue(styleName);
            if (styleValue && typeof styleValue === "string" && styleValue.length) {
                processedStyle[styleName] = styleValue;
            }
        }
    }

    if (
        processedStyle.display === "block" &&
        !(node.classList && node.classList.contains("oe-nested"))
    ) {
        delete processedStyle.display;
    }
    if (!processedStyle["box-sizing"]) {
        processedStyle["box-sizing"] = "border-box"; // This is by default with Bootstrap.
    }

    // The css generates all the attributes separately and not in simplified
    // form. In order to have a better compatibility (outlook for example) we
    // simplify the css tags. e.g. border-left-style: none; border-bottom-s ....
    // will be simplified in border-style = none
    for (const info of [
        { name: "margin" },
        { name: "padding" },
        { name: "border", suffix: "-style", defaultValue: "none" },
    ]) {
        const positions = ["top", "right", "bottom", "left"];
        const positionalKeys = positions.map(
            (position) => `${info.name}-${position}${info.suffix || ""}`
        );
        const styles = positionalKeys.map((key) => processedStyle[key]).filter((s) => s);
        const hasVariableStyle = styles.some(
            (style) => style.includes("calc(") || style.includes("var(")
        );
        const inherits = positionalKeys.some((key) =>
            ["inherit", "initial"].includes((processedStyle[key] || "").trim())
        );
        if (styles.length && !hasVariableStyle && !inherits) {
            const propertyName = `${info.name}${info.suffix || ""}`;
            processedStyle[propertyName] = positionalKeys.every(
                (key) => processedStyle[positionalKeys[0]] === processedStyle[key]
            )
                ? (processedStyle[propertyName] = processedStyle[positionalKeys[0]]) // top = right = bottom = left => property: [top];
                : positionalKeys
                      .map((key) => processedStyle[key] || info.defaultValue || 0)
                      .join(" "); // property: [top] [right] [bottom] [left];
            for (const prop of positionalKeys) {
                delete processedStyle[prop];
            }
        }
    }

    if (processedStyle["border-bottom-left-radius"]) {
        processedStyle["border-radius"] = processedStyle["border-bottom-left-radius"];
        delete processedStyle["border-bottom-left-radius"];
        delete processedStyle["border-bottom-right-radius"];
        delete processedStyle["border-top-left-radius"];
        delete processedStyle["border-top-right-radius"];
    }

    // If the border styling is initial we remove it to simplify the css tags
    // for compatibility. Also, since we do not send a css style tag, the
    // initial value of the border is useless.
    for (const styleName in processedStyle) {
        if (styleName.includes("border") && processedStyle[styleName] === "initial") {
            delete processedStyle[styleName];
        }
    }

    // text-decoration rule is decomposed in -line, -color and -style. This is
    // however not supported by many browser/mail clients and the editor does
    // not allow to change -color and -style rule anyway
    if (processedStyle["text-decoration-line"]) {
        processedStyle["text-decoration"] = processedStyle["text-decoration-line"];
        delete processedStyle["text-decoration-line"];
        delete processedStyle["text-decoration-color"];
        delete processedStyle["text-decoration-style"];
        delete processedStyle["text-decoration-thickness"];
    }

    // flexboxes are not supported in Windows Outlook
    for (const styleName in processedStyle) {
        if (styleName.includes("flex") || `${processedStyle[styleName]}`.includes("flex")) {
            delete processedStyle[styleName];
        }
    }

    return processedStyle;
}
let lastComputedStyleElement;
let lastComputedStyle;
/**
 * Return the value of the given style property on the given element. This
 * caches the last computed style so if it's called several times in a row for
 * the same element, we don't recompute it every time.
 *
 * @param {Element} element
 * @param {string} propertyName
 * @returns
 */
function _getStylePropertyValue(element, propertyName) {
    const computedStyle =
        lastComputedStyleElement === element ? lastComputedStyle : getComputedStyle(element);
    lastComputedStyleElement = element;
    lastComputedStyle = computedStyle;
    return computedStyle[propertyName] || element.style.getPropertyValue(propertyName);
}
/**
 * Equivalent to JQuery's `width` method. Returns the element's visible width.
 *
 * @param {Element} element
 * @returns {Number}
 */
function _getWidth(element) {
    return parseFloat(getComputedStyle(element).width.replace("px", "")) || 0;
}
/**
 * Equivalent to JQuery's `height` method. Returns the element's visible height.
 *
 * @param {Element} element
 * @returns {Number}
 */
function _getHeight(element) {
    return parseFloat(getComputedStyle(element).height.replace("px", "")) || 0;
}
/**
 * Hides the given node (or just its opening/closing tag) for Outlook with mso
 * conditional comments and, if needed, mso hide style.
 *
 * @param {Node} node
 * @param {false|'opening'|'closing'} [onlyHideTag=false]
 */
function _hideForOutlook(node, onlyHideTag = false) {
    if (!onlyHideTag) {
        let style = (node.getAttribute("style") || "").trim();
        if (style && !style.endsWith(";")) {
            style += ";";
        }
        node.setAttribute("style", `${style} mso-hide: all;`);
    }
    node[onlyHideTag === "closing" ? "append" : "before"](document.createComment("[if !mso]><!"));
    node[onlyHideTag === "opening" ? "prepend" : "after"](document.createComment("<![endif]"));
}
/**
 * Take a css style declaration return a "normalized" version of it (as a
 * standard object) for the purposes of emails. This means removing its styles
 * that are invalid, describe animations or aren't standard css (webkit
 * extensions). It also involves adding the "!important" suffix to styles that
 * have that priority, so they can be handled without access to the full
 * declaration.
 *
 * @param {CSSStyleDeclaration} style
 * @returns {Object} {[styleName]: string}
 */
function _normalizeStyle(style) {
    const normalizedStyle = {};
    for (const styleName of style) {
        const value = style[styleName];
        if (
            value &&
            !styleName.includes("animation") &&
            !styleName.includes("-webkit") &&
            typeof value === "string"
        ) {
            const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase());
            normalizedStyle[styleName] = style[normalizedStyleName];
            if (style.getPropertyPriority(styleName) === "important") {
                normalizedStyle[styleName] += " !important";
            }
        }
    }
    return normalizedStyle;
}
/**
 * Wrap a given element into a new parent, in place.
 *
 * @param {Element} element
 * @param {string} wrapperTag
 * @param {string} [wrapperClass] optional class to apply to the wrapper
 * @param {string} [wrapperStyle] optional style to apply to the wrapper
 * @returns {Element} the wrapper
 */
function _wrap(element, wrapperTag, wrapperClass, wrapperStyle) {
    const wrapper = document.createElement(wrapperTag);
    if (wrapperClass) {
        wrapper.className = wrapperClass;
    }
    if (wrapperStyle) {
        wrapper.style.cssText = wrapperStyle;
    }
    element.parentElement.insertBefore(wrapper, element);
    wrapper.append(element);
    return wrapper;
}

function isBlacklistedStyle(node, selector, key) {
    return (
        node.matches("table, thead, tbody, tfoot, tr, td, th") &&
        ["table", "thead", "tbody", "tfoot", "tr", "td", "th"].some((elName) =>
            selector.includes(elName)
        ) &&
        key.includes("color")
    );
}

function removeBlacklistedStyles(rule, node) {
    if (!rule.style) {
        return rule.style;
    }
    const styles = {};
    for (const [key, value] of Object.entries(rule.style)) {
        if (isBlacklistedStyle(node, rule.selector, key)) {
            continue;
        }
        styles[key] = value;
    }
    return styles;
}

export function splitSelectorAroundCommasOutsideParentheses(selector) {
    if (selector.indexOf(",") === -1) {
        return [selector].filter(Boolean);
    }
    const result = [];
    let start = 0;
    let depth = 0;
    let inString;
    for (let i = 0; i < selector.length; i++) {
        const char = selector[i];
        if (inString) {
            if (char === inString && selector[i - 1] !== "\\") {
                inString = undefined;
            }
            continue;
        }
        switch (char) {
            case "'":
            case '"':
                inString = char;
                break;
            case "(":
                depth++;
                break;
            case ")":
                depth--;
                if (depth < 0) {
                    return [selector];
                }
                break;
            case ",":
                if (depth === 0) {
                    result.push(selector.slice(start, i));
                    start = i + 1;
                }
                break;
        }
    }
    if (depth > 0) {
        return [selector];
    }
    result.push(selector.slice(start));
    return result.filter(Boolean);
}

/**
 * Corrects the `border-style` attribute in the provided inline style string.
 * This is specifically for Outlook, which displays borders even when their widths are set to 0px.
 * If all border widths are 0, the function updates `border-style` to `none`.
 *
 * @param {string} style - The inline style string to correct.
 * @returns {string} - The corrected inline style string.
 */
function correctBorderAttributes(style) {
    const stylesObject = style
        .replace(/\s+/g, " ")
        .split(";")
        .reduce((styles, styleString) => {
            const [attribute, value] = styleString.split(":").map((str) => str.trim());
            if (attribute) {
                styles[attribute] = value;
            }
            return styles;
        }, {});

    const BORDER_WIDTHS_ATTRIBUTES = [
        "border-bottom-width",
        "border-left-width",
        "border-right-width",
        "border-top-width",
    ];

    const isBorderStyleApplied = BORDER_WIDTHS_ATTRIBUTES.some(
        (attribute) => attribute in stylesObject
    );

    if (!isBorderStyleApplied) {
        return style;
    }

    const totalBorderWidth = BORDER_WIDTHS_ATTRIBUTES.reduce((totalWidth, attribute) => {
        const widthValue = stylesObject[attribute] || "0px";
        const numericWidth = parseFloat(widthValue.replace("px", "")) || 0;
        return totalWidth + numericWidth;
    }, 0);

    if (totalBorderWidth === 0) {
        let correctedStyle = style.trim();
        if (correctedStyle.slice(-1) != ";") {
            correctedStyle += ";";
        }
        correctedStyle = correctedStyle.replace(
            /(;|^)\s*border-style\s*:[^;]*(;|$)|$/,
            "$1border-style:none$2"
        );
        return correctedStyle;
    }

    if (/border-style\s*:/i.test(style)) {
        return style;
    }
    return style.trim().replace(/;?$/, "; border-style: solid;");
}
